package pl.puntin.punclient.networkadapter;

import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.net.ConnectException;
import java.net.InetSocketAddress;
import java.net.Socket;
import java.net.SocketAddress;
import java.net.SocketException;
import java.net.SocketTimeoutException;
import java.net.UnknownHostException;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.LinkedBlockingQueue;

import pl.puntin.punclient.messages.ConnectionLostMessage;
import pl.puntin.punclient.messages.IncomingMessageTypesMap;
import pl.puntin.punclient.messages.ListenerThreadIsDying;
import pl.puntin.punclient.messages.Message;
import pl.puntin.punclient.messages.SenderThreadIsDying;
import pl.puntin.punclient.messages.UnknowMessage;

public class NetworkAdapter
{
	/**
	 * Wstepnie 2 s.
	 */
	private static final int CONNECT_TIMEOUT = 2000;

	/**
	 * Kolejka blokujaca dla przychodzacych wiadomosci.
	 */
	private final BlockingQueue<Message> incomingQueue;

	/**
	 * Kolejka blokujaca dla wychodzacych wiadomsci.
	 */
	private final BlockingQueue<Message> outgoingQueue;
	/**
	 * Watek pobierajacy wiadomosci
	 */
	private final TcpListener listener;
	/**
	 * Watek wysylajacy wiadomosci
	 */
	 private final TcpSender sender;

	/**
	 * Socket
	 */
	private final Socket socket;

	private Thread listenerThread;

	private Thread senderThread;
	

	public NetworkAdapter(final String host, final int port)
			throws UnknownHostException, IOException, ConnectException, SocketTimeoutException
	{
		incomingQueue = new LinkedBlockingQueue<Message>();
		outgoingQueue = new LinkedBlockingQueue<Message>();
		
		SocketAddress sockaddr = new InetSocketAddress(host, port);
		socket = new Socket();
		socket.connect(sockaddr, CONNECT_TIMEOUT);
		listener = new TcpListener();
		sender = new TcpSender();

	}

	public void startListen()
	{
		listenerThread = new Thread(listener);
		listenerThread.start();
	}
	
	public void startSending()
	{
		senderThread = new Thread(sender);
		senderThread.start();
	}

	public void send(final Message message) throws InterruptedException
	{
		outgoingQueue.put(message);
	}

	public Message receive() throws InterruptedException
	{
		return incomingQueue.take();
	}
	
	/**
	 * To robi pan kamil
	 * 
	 * @author kakus
	 * 
	 */
	private class TcpListener implements Runnable
	{
		private static final int BUFFER_SIZE = 1000;
		private final InputStream inputStream;

		public TcpListener() throws IOException
		{
			inputStream = socket.getInputStream();
		}

		@Override
		public void run()
		{
			int numberOfBytesReadFormBuffer = 0;
			Message incompleteMessage = null;
			byte[] buffer = new byte[BUFFER_SIZE];
			while ( true )
			{
				try
				{
					/*
					 * Wczytanie danych z bufora
					 */
					int dataReceiveLenght = inputStream.read(buffer);
					numberOfBytesReadFormBuffer = 0;
					/*
					 * Nic nie wczytalismy to znaczy ze stracilismy polaczenie
					 */
					if (dataReceiveLenght <= 0)
					{
						incomingQueue.put(new ConnectionLostMessage());
						break;
					}
					
					/*
					 * Odczytujemy az skoncza nam sie bajty :P
					 */
					while(numberOfBytesReadFormBuffer < dataReceiveLenght)
					{
						Message message = null;
						/*
						 * Ostatnio odczytalismy wszstko wiec teraz mamy nowa wiadomosc w buforku
						 */
						if (incompleteMessage == null)
						{
							byte messageCode = buffer[numberOfBytesReadFormBuffer];
							/*
							 * Pobieramy klase wiadmosci
							 */
							Class<? extends Message> messageClass = IncomingMessageTypesMap
									.getClass(messageCode);
							/*
							 * Jesli trafimy jakis nieznay pakiet
							 */
							if (messageClass == null)
							{
								message = new UnknowMessage(messageCode);
							}
							else
							{
								/*
								 * Tworzymy obiekt klasy
								 */
								message = messageClass.newInstance();
							}
						}
						else
						{
							message = incompleteMessage;
						}
						
						/*
						 * Wiadomosc odczytuje swoje dane z bufora
						 */
						int dataLeftInBuffer = dataReceiveLenght-numberOfBytesReadFormBuffer;
						int messageBytesRead = message.read(buffer,
								numberOfBytesReadFormBuffer, dataLeftInBuffer);
						
						if (message.isComplete())
						{
							/*
							 * Wiadomosc odczytala wszystkie swoje dane
							 */
							numberOfBytesReadFormBuffer += messageBytesRead;
							/*
							 * Wkladam ja do kolejki
							 */
							incomingQueue.put(message);
							
							/*
							 * Jesli to bylo dokonczenie sciagania
							 */
							if(incompleteMessage != null)
								incompleteMessage = null;
						}
						else
						{
							/*
							 * Wiadomosc nie odczytala wszystkich swoich danych
							 */
							incompleteMessage = message;
							numberOfBytesReadFormBuffer += messageBytesRead;
						}
					}
				}
				catch (SocketException e)
				{
					try
					{
						incomingQueue.put(new ListenerThreadIsDying());
					}
					catch (InterruptedException e1)
					{
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
					break;
				}
				catch (IOException e)
				{
					e.printStackTrace();
					break;
				}
				catch (InterruptedException e)
				{
					try
					{
						incomingQueue.put(new ListenerThreadIsDying());
					}
					catch (InterruptedException e1)
					{
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
					break;

				}
				catch (SecurityException e)
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				catch (InstantiationException e)
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				catch (IllegalAccessException e)
				{
					// TODO Auto-generated catch block
					e.printStackTrace();
				}
				
			}
		}
	}

	/**
	 * To robi pan lukasz
	 * 
	 * @author lukasz
	 * 
	 */
	private class TcpSender implements Runnable
	{
		private final OutputStream outputStream;

		public TcpSender() throws IOException
		{
			outputStream = socket.getOutputStream();
		}

		@Override
		public void run()
		{
			while (true)
			{
				try
				{
					Message message = outgoingQueue.take();
					outputStream.write(message.getByteForm());
				}
				catch (SocketException e)
				{
					try
					{
						incomingQueue.put(new SenderThreadIsDying());
					}
					catch (InterruptedException e1)
					{
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
					break;
				}
				catch (IOException e)
				{
					try
					{
						incomingQueue.put(new SenderThreadIsDying());
					}
					catch (InterruptedException e1)
					{
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
					break;
				}
				catch (InterruptedException e)
				{
					try
					{
						incomingQueue.put(new SenderThreadIsDying());
					}
					catch (InterruptedException e1)
					{
						// TODO Auto-generated catch block
						e1.printStackTrace();
					}
					break;
				}
			}
		}

	}

	public void disconnect()
	{
		try
		{
			socket.close();
			senderThread.interrupt();
		}
		catch (IOException e)
		{
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
	}
}
